/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/graphics.hpp>
#include <baja/graphics/scenes.hpp>
#include <baja/observable.hpp>
#include <baja/app/input.hpp>
#include <baja/linear_interpolation.hpp>
#include <baja/tv_interpolation.hpp>
#include <journal/models/interfaces.h>
#include <journal/views/interfaces.h>
#include <journal/views/scene_state.h>
#include <journal/views/page.h>
#include <journal/views/list_page_layout.h>
#include <journal/views/grid_page_layout.h>
#include <baja/mru_cache.hpp>
#include <journal/app/constants.h>
#include <journal/views/scroll_bars.h>
#include <journal/views/quad_renderer.h>
#include <journal/views/notebook_camera.h>

namespace journal { namespace view {

namespace b = baja;
namespace a = baja::app;
namespace g = baja::graphics;
namespace gs = baja::graphics::scenes;
namespace gd = baja::graphics::dx11_1::details;

// I am maybe a loser,
// maybe a sinner.
// Either way, a perpetual beginner.
// -- The Dandy Warhols - You Come In Burned
class notebook :
    public inotebook,
    public inotebook_automation,
    public view::ipage_observer,
    public model::inotebook_model_observer,
    public std::enable_shared_from_this<inotebook>,
    public isettings_service_observer,
    public icamera_observer,
    public inotebook_camera_driver,
    public a::iui_service_observer,
    public icamera_service
{
public:
    BAJA_DECLARE_TAG("9C8A5679-62CD-421B-824D-53046FACC5F0")

private:
    enum class zoom_direction
    {
        in,
        out
    };

    // these act as flags in some cases
    enum class state : b::uint32
    {
        none = 0,
        idle = 1,
        panning = 2,
        starting = 4,
        transitioningFromGrid = 5,
        transitioningToGrid = 6,
        expanding = 7,
        collapsing = 8,

        // curling states
        autoCurling = 9,
        curling = 10,
        curlingBackwards = 11,

        // selection states
        startingSelecting = 12,
        selectingPages = 13,
        selectingPagesPanning = 14,
        movingSelectedPage = 15,
        finishedMovingSelectedPage = 16,
        finishedSelecting = 17,
        deletingPages = 18,

        // other states
        transitioningOther = 19,
        tapCurlTransition = 20,
        selectingWithLasso = 21,
        selectingWithLassoIdle = 22,
    };

    enum class sub_state : b::uint32
    {
        none = 0,
        collapsingThenTransitioningToGrid = 1,
        transitioningFromGridThenExpanding = 2,
        transitioningOtherThenExpanding = 3,
    };

    struct layout_data
    {
        layout_data() : id(layout_id::none) {}

        layout_id id;
        m::vector2d pagePixelResolution;
        std::wstring name; // for debugging purposes
        std::shared_ptr<ipage_layout> layout;
        view::load_level loadLevel;
    };

public:
    notebook(
        const size_t pageNumber,
        const std::shared_ptr<model::inotebook_model>& model,
        const std::shared_ptr<a::iinput_capture>& inputCapture
        );

    notebook(const notebook& other);
    ~notebook() { this->cleanup(); }
    static std::shared_ptr<notebook> create(
        const size_t pageNumber,
        const std::shared_ptr<model::inotebook_model>& model,
        const std::shared_ptr<a::iinput_capture>& inputCapture
        );

    // inotebook
    std::shared_ptr<model::inotebook_model> model() { return _notebookModel; }
    b::int32 addObserver(const std::shared_ptr<view::inotebook_observer>& observer) { return _observerSource += observer; }
    void removeObserver(const std::shared_ptr<view::inotebook_observer>& observer) { _observerSource -= observer; }
    void removeObserver(const b::int32 cookie) { _observerSource -= cookie; }
    size_t pageNumber() { return _pageNumber;}
    layout_id layoutId() { return _layoutId; }
    std::shared_ptr<gs::iscene_graph> sceneGraph() { return _sceneGraph; }
    bool saveThumbnailsAsync();
    bool areThumbnailsSaved();
    void generateTileImageAsync(std::function<void(const bool, const std::wstring&)> f);
    b::uint32 addNewPage();

    // irender_observer
    void onRender();
    void onResize(const a::resize_args& args);
    void onScreenChange(const a::screen_change_args& args);
    void onVisibilityChanged(const bool visible);

    // itick_observer
    void onTick();

    // iinput_observer
    void onPointer(const a::pointer_args& args, void* context = nullptr);
    void onGesture(const a::gesture_args& args, void* context = nullptr);
    void onKey(const a::key_args& args, void* context = nullptr);

    // model::inotebook_model_observer
    void onPageModelCreated(const std::shared_ptr<model::ipage_model>& model);
    void onPageLoadLevelSet(const std::shared_ptr<model::ipage_model>& page, const model::page_level currentLevel);

    // icamera_observer
    void onCameraChange(const std::shared_ptr<icamera>& c);

    // inotebook_camera_driver
    // icamera_service
    b::float32 pixelToWorld(const b::float32 pixel) { return pixel / _pixelToWorldRatio; }
    b::float32 worldToPixel(const b::float32 world) { return world * _pixelToWorldRatio; }
    const std::shared_ptr<icamera> camera() { return _sceneState.getCamera(); }
    const std::shared_ptr<ipage_layout> layout() { return _layoutData[_layoutId]->layout; }

    // iphoto_sink
    void onPhotoFileSelected(const std::shared_ptr<b::storage::istorage_file>& file);

    // iinput_capture
    void setCapture(const std::shared_ptr<a::iinput_observer>& observer) 
    { 
        _inputCapture.lock()->setCapture(std::static_pointer_cast<a::iinput_observer>(shared_from_this())); 
        _capture.set(observer);
    }

    void releaseCapture()
    { 
        _inputCapture.lock()->releaseCapture();
        _capture.release();
    }

    std::shared_ptr<iinput_observer> getCapture(void** context = nullptr)
    { 
        return _capture.get(); 
    }

    // isettings_service_observer
    void onBeforeSettingChange(const b::wchar* fullName);
    void onAfterSettingChange(const b::wchar* fullName);

    // iui_service_observer
    void onAppBarVisibilityChange(const bool visible);

    // view::ipage_observer
    void onPhotoAdded(const std::shared_ptr<view::iphoto>& photo)
    {
        // bubble it up
        _observerSource.raise(
            [&](const std::shared_ptr<view::inotebook_observer>& observer, void* context)
            {
                observer->onPhotoAdded(photo);
            });
    }

    void onPhotoRemoved(const std::shared_ptr<view::iphoto>& photo)
    {
        // bubble it up
        _observerSource.raise(
            [&](const std::shared_ptr<view::inotebook_observer>& observer, void* context)
            {
                observer->onPhotoRemoved(photo);
            });
    }

    void onThumbnailSaved(const std::shared_ptr<view::ipage>& page)
    {
        // bubble it up
        _observerSource.raise(
            [&](const std::shared_ptr<view::inotebook_observer>& observer, void* context)
            {
                observer->onThumbnailSaved(page);
            });
    }

    void iink_surface_observer::onDirty() { /* noop */ }
    void iphoto_observer::onDirty() { /* noop */ }

    // view::ipage_observer::iphoto_observer
    void onPhotoMoved(
        const std::shared_ptr<view::iphoto>& photo,
        const b::float32& newScale,
        const m::point2d& newPosition,
        const b::float32& newAngle
        )
    {
        // bubble it up
        _observerSource.raise(
            [&](const std::shared_ptr<view::inotebook_observer>& observer, void* context)
            {
                observer->onPhotoMoved(photo, newScale, newPosition, newAngle);
            });
    }

    void onPhotoSelected(const std::shared_ptr<view::iphoto>& photo) {}
    void onPhotoDeselected() {}

    // view::ipage_observer::iink_surface_observer
    void onStrokeAdded(const std::shared_ptr<model::iink_stroke_model>& newStroke)
    {
        // bubble it up
        _observerSource.raise(
            [&](const std::shared_ptr<view::inotebook_observer>& observer, void* context)
            {
                observer->onStrokeAdded(newStroke);
            });
    }

    // ishare_source
    void onShareImageAsync(copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f);
    void onCopyImageAsync(copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f);
    void onPasteImageAsync(const std::shared_ptr<view::ishared_image>& image, std::function<void()> f);

    // icommand_observer
    void onCommand(const view::command_id id, void* context = nullptr);

    // inotebook_automation
    std::shared_ptr<ipage> pageFromId(const b::uint32& pageId);

private:
    void init();
    void cleanup();
    void initializeLayouts();
    void initializeBackgroundTextures();
    void initializeCover(b::float32* startingScale);
    void setLayoutIdWithStack(const layout_id nextLayoutId);
    void setLayoutId(const layout_id layoutId);
    bool setStackExpansion(const stack_state stacked);
    std::shared_ptr<ipage_layout> currentLayout() { return _layoutData[_layoutId]->layout; }
    void renderViewInfo();
    bool handleGesture(const a::gesture_args& args);
    void updatePageSet(const m::vector2d& offsetXY, const m::vector2d focus);
    void updatePageSetInternal(page_set &newPageSet);
    void onPagePurged(const size_t& pageNumber, std::shared_ptr<ipage>& pageView);
    void setLayoutIdInternal(const layout_id layoutId);
    layout_id getNextLayout(const zoom_direction direction);
    void setStackExpansionInternal(const stack_state stackState);
    void updatePageNumberInternal();
    void goIdle();
    bool allowPageCurling();
    bool currentlyCurling();
    void deselectAll();
    bool startSelection(const m::point2d& position);
    bool handleSelectionTap(const m::point2d& position);
    void startLasso();
    bool handleLasso(const a::pointer_args& args);
    void renderLasso();
    void endLasso();
    void saveSharedImageAsync(const std::wstring& fileName, std::function<void(const std::shared_ptr<ishared_image>&)> f);
    std::shared_ptr<imemory_blob> generateImageFromSelection();
    bool getSelection(m::point2d& dimensions, m::point2d& origin);
    void renderFrame(const bool withChrome);

    struct page_slot;
    void enableSlot(const size_t& pageNumber, const bool preload = false);
    void disableSlot(const size_t& pageNumber);
    void autoCurlThisPageAway(bool forwards);

    // This is getting complex!
    void updatePagePositionInternal(const std::shared_ptr<ipage_layout>& layout, page_slot& slot);
    m::point3d UPPIMovingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot);
    m::point3d UPPIFinishedMovingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot);
    m::point3d UPPISelectedPages(m::point3d position, size_t pageNumber, const std::shared_ptr<paper_sheet_node> &pageNode);
    void UPPIDeletingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot, m::point3d *position, m::point3d *angle);
    void UPPILayoutTransition(const std::shared_ptr<ipage_layout>& layout, page_slot& slot, m::point3d *position, b::float32 *angle);

    void updateScrollBars();
    void updateCameraService();

private:
    b::observable_source<view::inotebook_observer> _observerSource;
    std::weak_ptr<a::iinput_capture> _inputCapture;
    b::capturable<a::iinput_observer> _capture;
    std::shared_ptr<model::inotebook_model> _notebookModel;
    b::int32 _notebookObserverCookie;
    b::int32 _settingsObserverCookie;

    std::shared_ptr<gs::iscene_graph> _sceneGraph;
    scene_state _sceneState;
    std::shared_ptr<iscene_references> _sceneReferences;
    std::shared_ptr<iinput_references> _inputReferences;
    std::shared_ptr<g::ifont> _font;

    std::vector<std::weak_ptr<layout_data>> _layoutDataSorted;
    std::map<layout_id, std::shared_ptr<layout_data>> _layoutData;

    size_t _pageNumber;
    layout_id _layoutId; // current layout
    size_t _layoutIndex; // current index
    
    // the difference between these two states is the first one is whatever is the current state in the screen
    // the second one is the one we should really have
    // this is important when we are transitioning from full to grid and we are expanded, _stackState will be stacked after the 
    // transition yet we'll want ot keep the _finalStackState as expanded so when we go back to full we'll automatically transition
    // to expanded.
    stack_state _stackState;  // this is the current stack state
    stack_state _finalStackState; // this is the stack state we should have

    b::mru_cache<size_t, std::shared_ptr<ipage>> _pageCache;
    page_set _pageSet; // current page set
    bool _pageSetDirty;

    b::uint64 _cameraChangeNumber;
    b::int32 _cameraObserverCookie;
    state _state;
    sub_state _subState;
    m::point2d _startOffsetXY;
    m::point3d _startPoint;
    b::float32 _startScale;
    size_t _startPage;
    m::point3d _startCurlBackwardsPoint;
    bool _keepAppBarAfterResorting;

    b::tv_interpolation_2d<m::point2d> _offsetXYAnimation;
    b::tv_interpolation _scaleAnimation;
    b::linear_interpolation _startCurlAnimation;
    b::tv_interpolation_3d<m::point3d> _anglesAnimation;
    b::tv_interpolation _distanceAnimation;
    b::tv_interpolation _transitionAnimation;
    b::float32 _animationEndTime;
    b::float32 _openingEndTime;
    bool _openingNotebook;
    b::float32 _transitionAnimationEndTime;
    b::float32 _transitionProgress;
    layout_id _transitionNextLayoutId;
    b::tv_interpolation _expansionAnimation;
    b::float32 _expansionAnimationEndTime;
    b::float32 _expansionProgress;
    bool _refreshPageSet;
    std::map<size_t, m::point3d> _randAngles;
    b::float32 _startingScale;
    bool _tapCurlTransitionForwards;

    struct page_slot
    {
        page_slot() : observerCookie(-1), isRemoved(false), transitionAngle(0) {}

        std::shared_ptr<ipage> pageView;
        b::int32 observerCookie;
        bool isRemoved;
        b::float32 transitionAngle;
        b::float32 maxAngleProgress; // point at which the angle will reach it's max (0..1)
    };

    // The iterator for this map is going to be in order by page number. This is needed
    // in notebook::onPointer
    typedef std::map<size_t, page_slot> page_slot_map;
    page_slot_map _slots;

    class page_selection
    {
    public:

        page_selection() : _state(state::not_selected) {}

        // Helpers because I don't want to make _state public
        void makeSelected() { _state = state::selected;	}
        void makeDeselected() { _state = state::not_selected; }
        bool isSelected()   { return _state == state::selected; }
        bool isSelecting()  { return _state == state::enabling;	}
        bool safeToRemove() { return _state == state::not_selected;	}

        void startTransition(b::float32 transitionLength, bool enabling)
        {
            b::float32 transitionStartTime = constants::general::currentTime();
            b::float32 transitionEndTime = transitionStartTime + transitionLength;
            if (enabling)
            {
                _li = b::linear_interpolation(transitionStartTime, transitionEndTime, 0.0f, 1.0f);
                _state = state::enabling;
            }
            else
            {
                _li = b::linear_interpolation(transitionStartTime, transitionEndTime, 1.0f, 0.0f);
                _state = state::disabling;
            }
        }

        b::float32 transitionProgress()
        {
            return _li.apply(constants::general::currentTime());
        }
        
        void possiblyFinishTransition()
        {
            if (_state == state::selected || _state == state::not_selected)
            {
                return;
            }

            b::float32 progress = this->transitionProgress();
            if (_state == state::enabling && progress == 1.f)
            {
                _state = state::selected;
            }
            else if (_state == state::disabling && progress == 0.f)
            {
                _state = state::not_selected;
            }
        }
        
    private:
        enum class state
        {
            not_selected,
            selected,
            enabling,
            disabling
        };

        b::linear_interpolation _li;
        state _state;
    };

    typedef std::map<size_t, page_selection> page_selection_map;
    page_selection_map _selections;
    std::shared_ptr<page_curl> _pageCurl;

    // cover
    std::shared_ptr<paper_sheet_node> _coverNode;
    std::shared_ptr<g::itexture> _coverTexture;

    std::shared_ptr<scroll_bars> _scrollBars;

    // idle time management
    b::stopwatch _idleTime;
    bool _saveThumbnails;

    // background
    std::shared_ptr<g::itexture> _finalScreenTexture;
    std::shared_ptr<g::itexture> _backTexture1;
    std::shared_ptr<g::itexture> _backTexture2;
    std::shared_ptr<g::render_target> _backRenderTarget;
    std::shared_ptr<quad_renderer> _backRenderer; 

    // camera
    std::shared_ptr<inotebook_camera> _notebookCamera;

    // camera service
    b::float32 _pixelToWorldRatio;

    // lasso
    m::point2d _lassoStart;
    m::point2d _lassoEnd;
};

inline std::shared_ptr<inotebook> createNotebook(
    const std::shared_ptr<model::inotebook_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<notebook> returnValue = notebook::create(
        constants::general::lastPageNumber(), 
        model, 
        inputCapture
        );

    return std::static_pointer_cast<inotebook>(returnValue);
}

}} // namespace journal::view